<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4.4.3-分支切换与 cleanup</title>
</head>

<body>
    <div id="container"></div>
</body>
<script>
    /**
     * 避免无限执行
     * - 新构造了 effectsToRun 集合并遍历它，代替直接遍历 effects 集合，
     *    从而避免了无限执行
     * 
     * 可以用如下简短的代码来表达问题原因：
     *  01 const set = new Set([1])
     *  02
     *  03 set.forEach(item => {
     *  04   set.delete(1)
     *  05   set.add(1)
     *  06   console.log('遍历中')
     *  07 })
     * 
     * 语言规范中对此有明确的说明：
     * - 在调用 forEach 遍历 Set 集合时，
     *    如果一个值已经被访问过了，但该值被删除并重新添加到集合，
     *    如果此时forEach 遍历没有结束，那么该值会重新被访问。
     * 
     * 解决办法 - 构造另外一个 Set 集合并遍历
     *  const set = new Set([1])
     *  const set2 = new Set(set)
     *  set2.forEach(item => {
     *      set.delete(1)
     *      set.add(1)
     *      console.log('遍历中')
     *  })
     * 
     * */ 

    // 存储副作用函数的桶
    const bucket = new WeakMap()
    // 原始数据
    const data = { ok: true, text: 'hello world' }
    // 对原始数据的代理
    const obj = new Proxy(data, {
        // 拦截读取操作
        get(target, key) {
            // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
            track(target, key)
            // 返回属性值
            return target[key]
        },
        // 拦截设置操作
        set(target, key, newVal) {
            // 设置属性值
            target[key] = newVal
            // 把副作用函数从桶里取出并执行
            trigger(target, key)
        }
    })

    // 在 get 拦截函数内调用 track 函数追踪变化
    function track(target, key) {
        // 没有 activeEffect，直接 return
        if (!activeEffect) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        // 把当前激活的副作用函数添加到依赖集合 deps 中
        deps.add(activeEffect)
        // deps 就是一个与当前副作用函数存在联系的依赖集合
        // 将其添加到 activeEffect.deps 数组中
        activeEffect.deps.push(deps)
    }
    // 在 set 拦截函数内调用 trigger 函数触发变化
    function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set(effects)  // 新增
        effectsToRun.forEach(effectFn => { effectFn() })  // 新增
        // effects && effects.forEach(fn => fn()) // 问题出在这句代码
    }
    
    // 用一个全局变量存储被注册的副作用函数
    let activeEffect
    // effect 函数用于注册副作用函数
    function effect(fn) {
        console.log('effect run')
        const effectFn = () => {
            // 调用 cleanup 函数完成清除工作
            cleanup(effectFn)
            // 当 effectFn 执行时，将其设置为当前激活的副作用函数
            activeEffect = effectFn;
            fn() // 执行 fn 函数
        }
        // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
        effectFn.deps = []
        // 执行副作用函数
        effectFn()
    }

    function cleanup(effectFn) {
        // 遍历 effectFn.deps 数组
        for (let i = 0; i < effectFn.deps.length; i++) {
            // deps 是依赖集合
            const deps = effectFn.deps[i]
            // 将 effectFn 从依赖集合中移除
            deps.delete(effectFn)
        }
        // 最后需要重置 effectFn.deps 数组
        effectFn.deps.length = 0
    }

    // 如下所示的方式使用 effect 函数
    effect(
        // 一个匿名的副作用函数
        () => {
            // 会触发响应式数据 obj.text 的读取操作
            // 进而触发代理对象 Proxy 的 get 拦截函数：
            console.log('effect trigger') // 会打印 2 次 !!
            document.body.innerText = obj.ok ? obj.text : 'not'
        }
    )

    setTimeout(() => {
        obj.ok = false
    }, 1000)

    // 这仍然会导致副作用函数重新执行，即使 document.body.innerText 的值不需要变化。
    setTimeout(() => {
        obj.text = 'hello vue3'
    }, 2000)
</script>

</html>